/*******************************************************************************
 * The MOSES project, model based software engineering of networked embedded systems.
 * 
 * Copyright (C) 2010-2013, University of Applied Sciences Wildau,
 * Telematics Research Group, http://www.tm.th-wildau.de/research-group
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses>.
 * 
 * Contributors:
 *     University of Applied Sciences Wildau - initial API and implementation
 * 
 * If you are interested in a commercial license please contact us via 
 * e-mail: ralf.vandenhouten@th-wildau.de.
 ******************************************************************************/
package de.thwildau.tm.moses.thrift.scoping;

import static org.eclipse.emf.common.util.URI.createURI;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.xtext.naming.IQualifiedNameProvider;
import org.eclipse.xtext.naming.QualifiedName;
import org.eclipse.xtext.resource.EObjectDescription;
import org.eclipse.xtext.resource.IEObjectDescription;
import org.eclipse.xtext.scoping.IScope;
import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider;
import org.eclipse.xtext.scoping.impl.ImportUriResolver;
import org.eclipse.xtext.scoping.impl.SimpleScope;

import com.google.inject.Inject;

import de.thwildau.tm.moses.thrift.thrift.Definition;
import de.thwildau.tm.moses.thrift.thrift.DefinitionRef;
import de.thwildau.tm.moses.thrift.thrift.Include;
import de.thwildau.tm.moses.thrift.thrift.ServiceRef;
import de.thwildau.tm.moses.thrift.thrift.Thrift;
import de.thwildau.tm.moses.thrift.thrift.ThrowsField;
import de.thwildau.tm.moses.thrift.util.ThriftElementFinder;

/**
 * This class contains custom scoping description for Thrift.
 * 
 * @author Thomas Kistel
 * @version $Revision: 759 $
 * @see http://www.eclipse.org/Xtext/documentation/latest/xtext.html#scoping on how and when to use it
 */
public class ThriftScopeProvider extends AbstractDeclarativeScopeProvider {

    @Inject
    private ImportUriResolver uriResolver;
    @Inject
    private IQualifiedNameProvider nameProvider;
    @Inject
    private LocalNamesProvider localNamesProvider;
    @Inject
    private ThriftElementFinder finder;


    /**
     * Creates the Scope for:
     * 
     * <pre>
     * DefinitionRef : definition = [Definition|QualifiedName];
     * </pre>
     */
    IScope scope_DefinitionRef_definition(DefinitionRef context, EReference ref) {
        return createScope(context, ref);
    }

    /**
     * Creates the Scope for:
     * 
     * <pre>
     * ServiceRef: service=[Service|QualifiedName];
     * </pre>
     */
    IScope scope_ServiceRef_service(ServiceRef context, EReference ref) {
        return createScope(context, ref);
    }

    /**
     * Creates the Scope for:
     * 
     * <pre>
     * ThrowsField: (fieldId=FieldID)? type=[Exception] name=ID ListSeparator?;
     * </pre>
     */
    IScope scope_ThrowsField_type(ThrowsField context, EReference ref) {
        return createScope(context, ref);
    }

    /**
     * Creates the Scope for the given <code>context</code>. The Scope always consist of the declared
     * definitions in the Thrift file of <code>context</code> and the declared definitions of all included
     * Thrift files.
     * 
     * @param context the current Context
     * @param ref
     * @return the Scope of the given Context
     */
    IScope createScope(EObject context, EReference ref) {
        Thrift root = finder.rootOf(context);
        Set<IEObjectDescription> descriptions = new HashSet<IEObjectDescription>();
        // add all types of the root element in this files to the Scope
        descriptions.addAll(typesIn(root));
        // add all types of the include definitions to this Scope
        descriptions.addAll(includedTypes(root, Definition.class));
        return createScope(descriptions);
    }
    
    private Collection<IEObjectDescription> typesIn(EObject root) {
        return children(root, Definition.class);
    }

    private <T extends Definition> Collection<IEObjectDescription> includedTypes(Thrift root, Class<T> targetType) {
        List<Include> allIncludes = finder.includesIn(root);
        if (allIncludes.isEmpty())
            return Collections.emptyList();
        return includedTypes(allIncludes, targetType);
    }

    private <T extends Definition> Collection<IEObjectDescription> includedTypes(List<Include> allIncludes,
            Class<T> targetType) {
        List<IEObjectDescription> descriptions = new ArrayList<IEObjectDescription>();
        for (Include anImport : allIncludes) {
            Resource importedResource = importedResourceFrom(anImport);
            Thrift importedRoot = finder.rootElementOf(importedResource);
            if (importedRoot != null) {
                descriptions.addAll(typesIn(importedRoot));
            }

        }
        return descriptions;
    }

    private <T extends Definition> Collection<IEObjectDescription> children(EObject root, Class<T> targetType) {
        return children(root, targetType, 0);
    }

    private <T extends Definition> Collection<IEObjectDescription> children(EObject root, Class<T> targetType, int level) {
        List<IEObjectDescription> descriptions = new ArrayList<IEObjectDescription>();
        for (EObject element : root.eContents()) {
            if (!targetType.isInstance(element))
                continue;
            List<QualifiedName> names = localNamesProvider.namesOf(element);
            int nameCount = names.size();
            for (int i = level; i < nameCount; i++) {
                descriptions.add(EObjectDescription.create(names.get(i), element));
            }
            descriptions.addAll(fullyQualifiedNamesOf(element));
            if (element instanceof de.thwildau.tm.moses.thrift.thrift.Enum) {
                descriptions.addAll(children((de.thwildau.tm.moses.thrift.thrift.Enum)element, level + 1));
            }
        }
        return descriptions;
    }

    private Collection<IEObjectDescription> children(de.thwildau.tm.moses.thrift.thrift.Enum root, int level) {
        List<IEObjectDescription> descriptions = new ArrayList<IEObjectDescription>();
        for (EObject element : root.eContents()) {
            List<QualifiedName> names = localNamesProvider.namesOf(element);
            int nameCount = names.size();
            for (int i = level; i < nameCount; i++) {
                // here it is only possible to create the Description with 'root' (i.e. the outer element),
                // because
                // a type EnumElement cannot be assigned to reference DefinitionRef
                IEObjectDescription objDesc = EObjectDescription.create(names.get(i), root);
                descriptions.add(objDesc);
            }
            // descriptions.addAll(fullyQualifiedNamesOf(element));
        }
        return descriptions;
    }

    private Collection<IEObjectDescription> fullyQualifiedNamesOf(EObject obj) {
        List<IEObjectDescription> descriptions = new ArrayList<IEObjectDescription>();
        QualifiedName fqn = nameProvider.getFullyQualifiedName(obj);
        descriptions.add(EObjectDescription.create(fqn, obj));
        return descriptions;
    }

    private static IScope createScope(Iterable<IEObjectDescription> descriptions) {
        return new SimpleScope(descriptions, false);
    }

    private Resource importedResourceFrom(Include anImport) {
        Thrift rootOf = finder.rootOf(anImport);
        Resource rootResource = rootOf.eResource();
        ResourceSet resourceSet = rootResource.getResourceSet();
        String uriString = uriResolver.apply(anImport);
        URI importUri = createURI(uriString);
        try {
            return resourceSet.getResource(importUri, true);
        } catch (Throwable t) {
            return null;
        }
    }    
}
